using BepInEx;
using BepInEx.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using VisualmodBep.Helper;

namespace VisualmodBep.Gui;

/**
 * In-game GUI base class, must be a singleton
 * @ref https://docs.unity3d.com/ScriptReference/GUILayout.html
 * @ref https://docs.unity3d.com/cn/current/Manual/GUIScriptingGuide.html
 */
public class GuiBase<Tp> : MonoBehaviour where Tp : GuiBase<Tp>, new() {
    #region Layout
    protected Vector2 windowPos;
    protected Vector2 windowSize;
    protected Vector2 titleSize;

    public GuiEm guiEm = GuiEm.Default;
    public float em { get => guiEm.em; }

    public string windowTitle { get; set; }
    #endregion

    #region Window
    public delegate void PanelDrawDelegate(int flag);

    protected NavbarComponent windowPanelDiv;
    protected PanelDrawDelegate[] windowPanelDraw;
    protected int[] windowPanelParam;
    protected Texture2D windowTex;

    private int windowWid;
    private static GameObject windowObj;
    #endregion

    #region LayoutVarient
    private Vector2 scrollPos;
    private Vector2 windowMove;
    private bool dragAndMoving;

    protected bool active;
    protected bool loaded;
    internal bool initCalled;
    #endregion

    protected static Func<string, string> T = Translation.T;

    private static Tp _instance;
    public static Tp Instance { get => _instance ??= Factory(); }
    public static Tp Factory() {
        Log.LogInfo($"GuiBase Factory {typeof(Tp).Name}");
        if (windowObj == null) {
            windowObj = new GameObject("GuiObject");
        }
        Tp instance = windowObj.AddComponent<Tp>();
        return instance;
    }

    public GuiBase() {
        Log.LogInfo("GuiBase ctor");
        active = loaded = initCalled = false;
    }

    private void Awake() {
        Log.LogInfo("GuiBase Awake");
        DontDestroyOnLoad(this);
    }

    public static void CloseWindow() {
        if (_instance) {
            _instance.SetWindowActive(false);
        }
    }

    private void Draw() {
        if (!loaded) {
            Log.LogError("GuiBase Not Loaded");
            return;
        }
        if (active) {
            guiEm.Check();
            MoveWindow();
            _draw_before(); // derived class _draw_before method
            Rect rect = new Rect(windowPos.x, windowPos.y, windowSize.x, windowSize.y);
            GUI.DrawTexture(rect, windowTex);
            GUI.Window(windowWid, rect, DrawWindow, windowTitle);
            _draw_after(); // derived class _draw_after method
        }
    }

    /// override this method for additional drawing
    protected virtual void _draw_before() {
        return;
    }

    /// override this method for additional drawing
    protected virtual void _draw_after() {
        return;
    }

    void DrawWindow(int wid) {
        float paddingX = 0.8f * em;
        float paddingY = 1.6f * em;
        GUILayout.BeginArea(new Rect(paddingX, paddingY, windowSize.x, windowSize.y));
        GUILayout.BeginVertical();
        windowPanelDiv.OnGUI();
        scrollPos = GUILayout.BeginScrollView(scrollPos, GUILayout.Width(windowSize.x - 2f * paddingX), GUILayout.Height(windowSize.y - 2f * paddingY));
        int activePanel = windowPanelDiv.Index;
        windowPanelDraw[activePanel](windowPanelParam[activePanel]);
        GUILayout.EndScrollView();
        GUILayout.EndVertical();
        GUILayout.EndArea();
    }

    /// Initialize will call Reset once
    public void Initialize() {
        if (initCalled) {
            Log.LogInfo("GuiBase Initialize called");
            return;
        }
        initCalled = true;
        Log.LogInfo("GuiBase Initialize");
        windowPos = new Vector2(240, 100);
        windowSize = new Vector2(600, 720);
        windowTex = new Texture2D(16, 16);
        windowTex.SetPixels(Enumerable.Repeat(new Color(0f, 0f, 0f, .8f), 256).ToArray());
        windowTex.Apply();
        windowWid = 451919810;
        titleSize = new Vector2(windowSize.x, 1.25f * em); // for MoveWindow checking

        _init(); // user defined

        Reset();
        loaded = true;
        guiEm.Refresh(); // trigger once
    }

    /// override this method
    protected virtual void _init() {
        windowPanelDiv = new NavbarComponent(new string[] { "Settings" }, "UI.Panel.", 0, 7f);
        windowPanelDraw = new PanelDrawDelegate[] { (int flag) => { } };
        windowPanelParam = new int[] { 0 };
        return;
    }

    private void MoveWindow() {
        Vector2 mouse = Input.mousePosition;
        mouse.y = Screen.height - mouse.y;

        if (dragAndMoving) {
            if (Input.GetMouseButton(0)) {
                windowPos.x = Mathf.Clamp(windowMove.x + mouse.x, 10, Screen.width - 120);
                windowPos.y = Mathf.Clamp(windowMove.y + mouse.y, 10, Screen.height - 120);
            } else {
                dragAndMoving = false;
            }
        } else if (new Rect(windowPos.x, windowPos.y, titleSize.x, titleSize.y).Contains(mouse) && Input.GetMouseButton(0)) {
            dragAndMoving = true;
            windowMove = windowPos - mouse;
        }
    }

    private void OnDestory() {
        Log.LogInfo("GuiBase OnDestory");
    }

    private void OnGUI() {
        // Log.LogInfo("GuiBase OnGui");
        Draw(); // base class draw
    }

    private void Reset() {
        scrollPos = new Vector2(0, 0);
        dragAndMoving = false;
        SetWindowActive(false);
        _reset(); // user defined
    }

    /// override this method
    protected virtual void _reset() {
        return;
    }

    protected void SetWindowActive(bool value) {
        active = loaded && value;
        Log.LogInfo($"{typeof(Tp).Name} SetWindowActive {value}");
        windowObj.SetActive(active);
        if (active) {
            FloatComponent.AllInstances.ForEach(f => { f.ValueNeedSet = true; });
        }
    }

    /// @return true if value changed
    protected bool ToggleConfigEntry(ConfigEntry<bool> entry, string title, string keyName, params GUILayoutOption[] options) {
        bool toggle = GUILayout.Toggle(entry.Value, title, options);
        if (toggle != entry.Value) {
            Log.LogInfo($"Toggle ConfigEntry<bool> \"{keyName}\" {toggle}");
            entry.Value = toggle;
            return true;
        } else {
            return false;
        }
    }

    protected void DrawAligmentText(string text, TextAnchor alignment, string tooltip = null, params GUILayoutOption[] options) {
        TextAnchor prev = GUI.skin.label.alignment;
        GUI.skin.label.alignment = alignment;
        GUILayout.Label(new GUIContent(text, tooltip), options: options);
        GUI.skin.label.alignment = prev;
    }

    public static void ToggleWindow() {
        if (_instance && _instance.loaded) {
            _instance.SetWindowActive(!_instance.active);
        }
    }
}

public class GuiEm {
    public float em // basic size unit, default 12
    {
        get => _em;
        set {
            if (_em != value) {
                _em = value;
                refreshEm = true;
            }
        }
    }

    public static GuiEm Default = new GuiEm();

    private bool refreshEm = false;
    private float _em = 12f;

    public void Check() {
        if (refreshEm) {
            refreshEm = false;
            GUI.skin.button.fontSize = Math.Max((int)em, 7);
            GUI.skin.verticalScrollbar.fixedWidth = 10f;
            GUI.skin.verticalScrollbar.contentOffset = new Vector2(14f, 0f);
            GUI.skin.verticalScrollbarThumb.fixedWidth = 10f;
            GUI.skin.verticalScrollbarThumb.contentOffset = new Vector2(14f, 0f);
        }
    }

    public void Refresh() {
        refreshEm = true;
    }
}

public class ComponentBase<Tp> : Helper.AllInstance<Tp> where Tp : class {
    public GuiEm guiEm = GuiEm.Default;

    protected float em { get => guiEm.em; }
    protected static Func<string, string> T = Translation.T;

    public static bool EnterKeyDown() {
        Event e = Event.current;
        return e.type == EventType.KeyDown && (e.keyCode == KeyCode.Return || e.keyCode == KeyCode.KeypadEnter);
    }
}

public class FloatComponent : ComponentBase<FloatComponent> {
    public string label;

    public float min;
    public float max;
    public float Value { get; private set; }
    public bool ValueNeedSet { get; set; } // need to be set

    public DrawType mode;

    [Flags]
    public enum DrawType : uint {
        DrawInt = 0x1u, // set: render integer value, unset: float
        DrawSlider = 0x2u, // set: draw slider
        DrawLabelTranslation = 0x4u, // set: label as translation key
        DrawLabelEnd = 0x8u // set: draw label at end, unset: at start
    }
    public const DrawType DefaultDrawType = DrawType.DrawSlider | DrawType.DrawLabelTranslation | DrawType.DrawLabelEnd;

    public FloatComponent(string label, float min, float max, DrawType mode = DefaultDrawType, float sliderWidthEm = 15) {
        InstanceAdd(this);
        this.label = label;
        this.min = min;
        this.max = max;
        this.mode = mode;
        this.sliderWidthEm = sliderWidthEm;
        this.sliderValue = min; // falsy value
        this.ValueNeedSet = true;
    }

    public bool OnGUI(float value) {
        bool changed = false;
        // set to config value
        if (ValueNeedSet) {
            UpdateValue(value);
        }
        GUILayout.BeginHorizontal(); // inline component

        // front label
        if (!mode.HasFlag(DrawType.DrawLabelEnd)) {
            GUILayout.Label(mode.HasFlag(DrawType.DrawLabelTranslation) ? T(label) : label);
            GUILayout.Space(1f * em);
        }

        // draw slider and listen change
        if (mode.HasFlag(DrawType.DrawSlider)) {
            GUILayout.BeginVertical(GUILayout.Width(sliderWidthEm * em));
            GUILayout.Space(0.75f * em);
            sliderValue = GUILayout.HorizontalSlider(sliderValue, min, max);
            if (sliderValue != Value) {
                Log.LogInfo($"FloatComponent {label} Silder {sliderValue}");
                changed = true;
                UpdateValue(sliderValue);
            }
            GUILayout.EndVertical();
            GUILayout.Space(1f * em);
        }

        // handle Enter key and draw input
        if (EnterKeyDown() && textValue != previousTextValue) {
            Log.LogInfo($"FloatComponent {label} Enter \"{textValue}\"");
            if (textValue.Length > 0) {
                float newValue;
                if (float.TryParse(textValue.Trim(), out newValue)) {
                    if (mode.HasFlag(DrawType.DrawInt)) {
                        newValue = Mathf.Clamp(Mathf.Round(newValue), min, max);
                    } else {
                        newValue = Mathf.Clamp(newValue, min, max);
                    }
                    if (newValue != Value) {
                        changed = true;
                        UpdateValue(newValue); // update text
                    } else {
                        textValue = previousTextValue; // reset text to previous
                    }
                } else {
                    Log.LogError($"  TextField invalid value \"{textValue}\"");
                    ValueNeedSet = true; // reset value to previous
                }
            } else {
                ValueNeedSet = true; // reset value to default
            }
        }
        textValue = GUILayout.TextField(textValue, 100, GUILayout.Width(5f * em));

        // back label
        if (mode.HasFlag(DrawType.DrawLabelEnd)) {
            GUILayout.Space(1f * em);
            GUILayout.Label(mode.HasFlag(DrawType.DrawLabelTranslation) ? T(label) : label);
        }
        GUILayout.EndHorizontal();
        return changed;
    }

    private float sliderValue;
    private string textValue;
    private string previousTextValue;
    private float sliderWidthEm;

    private void UpdateValue(float value) {
        // Log.LogInfo($"FloatComponent UpdateValue {label} {value}");
        Value = value; // don't care about clamping
        if (mode.HasFlag(DrawType.DrawInt)) {
            sliderValue = Mathf.Round(value);
            textValue = ((int)value).ToString();
        } else {
            sliderValue = value;
            textValue = value.ToString("F3");
        }
        previousTextValue = textValue;
        ValueNeedSet = false;
    }

    public void SetValue(float value) {
        Log.LogInfo($"FloatComponent SetValue {label} {value}");
        UpdateValue(Mathf.Clamp(value, min, max));
    }
}

public class SelectComponent : ComponentBase<SelectComponent> {
    public string label;
    private string ns; // translation namespace

    public string[] Options { get; private set; }
    public int Index { get => selected; }
    public string Value { get => Options[selected]; }
    public bool ValueNeedSet { get; set; } // need to be set

    public SelectComponent(string label, string[] options, string ns = "") {
        InstanceAdd(this);
        this.label = label;
        if (options.Length == 0) {
            Log.LogError("SelectComponent ctor no options");
        }
        this.Options = options;
        this.ns = ns;
        this.selected = 0; // falsy value
        this.ValueNeedSet = true;
    }

    public bool OnGUI(int index) {
        return OnGUI(Options[index]);
    }

    public bool OnGUI(string value) {
        bool changed = false;
        // set to config value
        if (ValueNeedSet) {
            int i = Array.FindIndex(Options, s => s == value);
            this.selected = i == -1 ? 0 : i;
            ValueNeedSet = false;
        }
        GUILayout.BeginHorizontal();
        if (label.Length > 0) {
            GUILayout.Label(T(label));
        }
        // GUILayout.FlexibleSpace();
        int selected = GUILayout.SelectionGrid(this.selected, Translation.T(Options, ns), Math.Min(Options.Length, 5), "toggle");
        if (selected != this.selected) {
            changed = true;
            this.selected = selected;
        }
        GUILayout.EndHorizontal();
        return changed;
    }

    private int selected { get; set; }
}

public class NavbarComponent : ComponentBase<NavbarComponent> {
    public string ns; // translation namespace

    public string[] Options { get; private set; }
    public int Index { get => selected; }
    public string Value { get => Options[selected]; }

    public float widthEm;

    public NavbarComponent(string[] options, string ns, int initValue, float widthEm) {
        InstanceAdd(this);
        if (options.Length == 0) {
            Log.LogError("NavbarComponent ctor no options");
        }
        this.Options = options;
        this.ns = ns;
        this.selected = Mathf.Clamp(initValue, 0, Options.Length);
        this.widthEm = widthEm;
    }

    public NavbarComponent(string[] options, string ns = "") : this(options, ns, 0, 7f) { }

    public NavbarComponent(IEnumerable<string> options, string ns = "") : this(options.ToArray(), ns, 0, 7f) { }

    public bool OnGUI() {
        bool changed = false;

        GUILayout.BeginHorizontal();
        GUIStyle buttonStyle = new GUIStyle(GUI.skin.button);
        for (int i = 0; i < Options.Length; i++) {
            if (i == selected) {
                buttonStyle.normal.textColor = new Color32(121, 134, 203, 255);
            } else {
                buttonStyle.normal.textColor = Color.white;
            }
            if (GUILayout.Button(T(ns + Options[i]), buttonStyle, GUILayout.Width(widthEm * em), GUILayout.Height(2f * em))) {
                if (i != selected) {
                    selected = i;
                    changed = true;
                }
            }
        }
        GUILayout.Space(0.5f * em);
        GUILayout.EndHorizontal();
        return changed;
    }

    private int selected { get; set; }

    public void SetIndex(int value) {
        if (value < 0 || value > Options.Length) {
            Log.LogError("NavbarComponent SetIndex error");
            return;
        }
        selected = value;
    }
}

public class KeyboardShortcutComponent : ComponentBase<KeyboardShortcutComponent> {
    public KeyboardShortcut Value { get => shortcut; }

    public string label;
    public KeyboardShortcut defaultShortcut;
    public IEnumerable<KeyCode> KeysToCheck;
    public float lableWidthEm = 16f;

    private bool keyboardToSet = false;
    private KeyboardShortcut shortcut;
    private GUIStyle buttonStyle;

    public KeyboardShortcutComponent(string label, KeyboardShortcut currentShortcut, KeyboardShortcut defaultShortcut) {
        InstanceAdd(this);
        this.label = label;
        this.shortcut = currentShortcut;
        this.defaultShortcut = defaultShortcut;
        // @todo dynamic limit
        this.KeysToCheck = UnityInput.Current.SupportedKeyCodes.Except(new[] { KeyCode.Mouse0, KeyCode.None }).ToArray();
        buttonStyle = null;
    }

    public KeyboardShortcutComponent(string label, ConfigEntry<KeyboardShortcut> config) : this(label, config.Value, (KeyboardShortcut)config.DefaultValue) { }

    public bool OnGUI() {
        if (buttonStyle == null) {
            buttonStyle = new GUIStyle(GUI.skin.button); // @todo dynamic style
            buttonStyle.normal.background = GUI.skin.textField.normal.background;
        }
        if (keyboardToSet) {
            buttonStyle.normal.textColor = new Color32(121, 134, 203, 255);
        } else {
            buttonStyle.normal.textColor = Color.white;
        }
        bool changed = false;
        GUILayout.BeginHorizontal();
        GUILayout.Label(T(label), GUILayout.MinWidth(lableWidthEm * em), GUILayout.Height(2f * em));
        if (keyboardToSet) {
            bool enabled = GUI.enabled;
            GUI.enabled = false;
            GUILayout.Button(T("UI.Hotkey.PressCombination"), buttonStyle, GUILayout.ExpandWidth(true), GUILayout.Height(2f * em));
            GUI.enabled = enabled; // restore
            GUIUtility.keyboardControl = -1; // unfocus
            foreach (KeyCode key in KeysToCheck) {
                if (UnityInput.Current.GetKeyUp(key)) {
                    InternalSetShortcut(new KeyboardShortcut(key, KeysToCheck.Where(UnityInput.Current.GetKey).ToArray()));
                    break;
                }
            }
            if (GUILayout.Button(T("Cancel"), GUILayout.ExpandWidth(false))) {
                keyboardToSet = false;
            }
        } else {
            if (GUILayout.Button(shortcut.ToString(), buttonStyle, GUILayout.ExpandWidth(true), GUILayout.Height(2f * em))) {
                foreach (KeyboardShortcutComponent component in AllInstances) {
                    component.keyboardToSet = false; // maybe another component is active
                }
                keyboardToSet = true;
            }

            if (GUILayout.Button(T("Clear"), GUILayout.ExpandWidth(false), GUILayout.Height(2f * em))) {
                InternalSetShortcut(KeyboardShortcut.Empty);
            }
        }
        if (GUILayout.Button(T("Reset"), GUILayout.ExpandWidth(false), GUILayout.Height(2f * em))) {
            InternalSetShortcut(defaultShortcut);
        }
        GUILayout.EndHorizontal();
        return changed;
    }

    public void SetShortcut(KeyboardShortcut keyboardShortcut) {
        if (!shortcut.Equals(keyboardShortcut)) {
            shortcut = keyboardShortcut;
        }
    }

    private bool InternalSetShortcut(KeyboardShortcut keyboardShortcut) {
        keyboardToSet = false;
        if (!shortcut.Equals(keyboardShortcut)) {
            shortcut = keyboardShortcut;
            Log.LogInfo($"KeyboardShortcut {label} set \"{shortcut.ToString()}\" ");
            return true;
        }
        return false;
    }
}
